package com.ityuwen.mengyagushi.search.service.impl;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.aggregations.Aggregate;
import co.elastic.clients.elasticsearch._types.aggregations.LongTermsBucket;
import co.elastic.clients.elasticsearch._types.aggregations.TopHitsAggregate;
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.Operator;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.CompletionSuggestOption;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.HitsMetadata;
import co.elastic.clients.elasticsearch.core.search.Suggestion;
import co.elastic.clients.json.JsonData;
import com.ityuwen.mengyagushi.album.client.AlbumInfoFeignClient;
import com.ityuwen.mengyagushi.album.client.CategoryFeignClient;
import com.ityuwen.mengyagushi.common.constant.RedisConstant;
import com.ityuwen.mengyagushi.common.result.Result;
import com.ityuwen.mengyagushi.model.album.AlbumAttributeValue;
import com.ityuwen.mengyagushi.model.album.AlbumInfo;
import com.ityuwen.mengyagushi.model.album.BaseCategory3;
import com.ityuwen.mengyagushi.model.album.BaseCategoryView;
import com.ityuwen.mengyagushi.model.search.AlbumInfoIndex;
import com.ityuwen.mengyagushi.model.search.AttributeValueIndex;
import com.ityuwen.mengyagushi.model.search.SuggestIndex;
import com.ityuwen.mengyagushi.model.user.UserInfo;
import com.ityuwen.mengyagushi.query.search.AlbumIndexQuery;
import com.ityuwen.mengyagushi.search.service.SearchService;
import com.ityuwen.mengyagushi.user.client.UserInfoFeignClient;
import com.ityuwen.mengyagushi.vo.album.AlbumStatVo;
import com.ityuwen.mengyagushi.vo.search.AlbumInfoIndexVo;
import com.ityuwen.mengyagushi.vo.search.AlbumSearchResponseVo;
import com.ityuwen.mengyagushi.vo.search.HomePageVo;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;


@Slf4j
@Service
@SuppressWarnings({"unchecked", "rawtypes"})
public class SearchServiceImpl implements SearchService {

    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;

    @Autowired
    private AlbumInfoFeignClient albumInfoFeignClient;

    @Autowired
    private UserInfoFeignClient userInfoFeignClient;

    @Autowired
    private CategoryFeignClient categoryFeignClient;

    @Autowired
    private ElasticsearchClient elasticsearchClient;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    public ExecutorService executorService;

    @Override
    public void upper(Long albumId) {
        AlbumInfoIndex albumInfoIndex = new AlbumInfoIndex();

        // 设置专辑信息
        Result<AlbumInfo> albumInfoResult = this.albumInfoFeignClient.getAlbumInfo(albumId);
        AlbumInfo albumInfo = albumInfoResult.getData();
        BeanUtils.copyProperties(albumInfo, albumInfoIndex);

        // 设置主播
        Result<UserInfo> userInfoResult = this.userInfoFeignClient.getUserInfo(albumInfo.getUserId());
        Assert.notNull(userInfoResult, "数据导入时：获取主播信息失败！");
        UserInfo userInfo = userInfoResult.getData();
        if (userInfo != null) {
            albumInfoIndex.setAnnouncerName(userInfo.getNickname());
        }

        // 设置三级分类的id
        Result<BaseCategoryView> baseCategoryViewResult = this.categoryFeignClient.findBaseCategoryView(albumInfo.getCategory3Id());
        Assert.notNull(baseCategoryViewResult, "数据导入时：获取三级分类信息失败！");
        BaseCategoryView baseCategoryView = baseCategoryViewResult.getData();
        if (baseCategoryView != null) {
            albumInfoIndex.setCategory1Id(baseCategoryView.getCategory1Id());
            albumInfoIndex.setCategory2Id(baseCategoryView.getCategory2Id());
            albumInfoIndex.setCategory3Id(baseCategoryView.getCategory3Id());
        }

        // 设置统计信息
        Integer playNum = (new Random().nextInt(10) + 1) * 1000000;
        Integer subscribeNum = (new Random().nextInt(10) + 1) * 1000000;
        Integer buyNum = (new Random().nextInt(10) + 1) * 1000000;
        Integer commentNum = (new Random().nextInt(10) + 1) * 1000000;
        albumInfoIndex.setPlayStatNum(playNum);
        albumInfoIndex.setSubscribeStatNum(subscribeNum);
        albumInfoIndex.setBuyStatNum(buyNum);
        albumInfoIndex.setCommentStatNum(commentNum);
        albumInfoIndex.setHotScore(playNum * 0.1 + subscribeNum * 0.2 + buyNum * 0.4 + commentNum * 0.3);

        // 设置标签信息
        Result<List<AlbumAttributeValue>> attributeValuesResult = this.albumInfoFeignClient.findAttributeValuesByAlbumId(albumInfo.getId());
        Assert.notNull(attributeValuesResult, "数据导入时：获取标签信息失败！");
        List<AlbumAttributeValue> albumAttributeValues = attributeValuesResult.getData();
        if (!CollectionUtils.isEmpty(albumAttributeValues)){
            // 把标签AlbumAttributeValue转化成AttributeValueIndex
            albumInfoIndex.setAttributeValueIndexList(albumAttributeValues.stream().map(albumAttributeValue -> {
                AttributeValueIndex attributeValueIndex = new AttributeValueIndex();
                BeanUtils.copyProperties(albumAttributeValue, attributeValueIndex);
                return attributeValueIndex;
            }).collect(Collectors.toList()));
        }

        this.elasticsearchTemplate.save(albumInfoIndex);
    }

    @Override
    public void lower(String albumId) {
        this.elasticsearchTemplate.delete(albumId, AlbumInfoIndex.class);
    }

    @Override
    public List<HomePageVo> channel(Long category1Id) {

        // 1.根据一级分类id查询置顶频道
        Result<List<BaseCategory3>> topBaseCategory3Result = this.categoryFeignClient.findTopBaseCategory3(category1Id);
        Assert.notNull(topBaseCategory3Result, "获取首页热门数据时：查询置顶频道失败！");
        List<BaseCategory3> baseCategory3s = topBaseCategory3Result.getData();
        // 如果热门频道数据为空，则直接返回
        if (CollectionUtils.isEmpty(baseCategory3s)){
            return null;
        }
        // 把list集合转化成map<category3Id, category3>集合
        Map<Long, BaseCategory3> map = baseCategory3s.stream().collect(Collectors.toMap(BaseCategory3::getId, baseCategory3 -> baseCategory3));

        // 需要把三级分类集合转换成 FieldValue集合
        List<FieldValue> category3Ids = baseCategory3s.stream().map(baseCategory3 -> FieldValue.of(baseCategory3.getId())).collect(Collectors.toList());

        // 拼接DSL语句
        SearchRequest request = SearchRequest.of(s -> s
                .index("albuminfo")
                .size(0)
                .query(q -> q.terms(t -> t.field("category3Id").terms(ts -> ts.value(category3Ids))))
                .aggregations("categoryAgg", a -> a.terms(t -> t.field("category3Id"))
                        .aggregations("hotScoreTop", t -> t
                                .topHits(h -> h
                                        .size(6)
                                        .sort(st -> st.field(f -> f
                                                .field("hotScore")
                                                .order(SortOrder.Desc))))))
        );

        try {
            // 执行搜索
            System.out.println(request.toString());
            SearchResponse<AlbumInfoIndex> response = this.elasticsearchClient.search(request, AlbumInfoIndex.class);

            // 解析搜索结果集
            Map<String, Aggregate> aggregations = response.aggregations();
            if (CollectionUtils.isEmpty(aggregations)){
                return null;
            }
            // 获取分类聚合
            Aggregate aggregate = aggregations.get("categoryAgg");
            if (aggregate == null){
                return null;
            }
            // 获取三级分类桶集合
            List<LongTermsBucket> buckets = aggregate.lterms().buckets().array();
            if (!CollectionUtils.isEmpty(buckets)){
                return buckets.stream().map(bucket -> {
                    HomePageVo homePageVo = new HomePageVo();

                    // 热门的三级分类（一个频道）
                    long category3Id = bucket.key();
                    // 根据三级分类id到map中获取
                    homePageVo.setBaseCategory3(map.get(category3Id));
                    // 对应频道下的热门数据
                    Map<String, Aggregate> subAggs = bucket.aggregations();
                    if (CollectionUtils.isEmpty(subAggs)){
                        return homePageVo;
                    }
                    Aggregate subAgg = subAggs.get("hotScoreTop");
                    TopHitsAggregate topHitsAggregate = subAgg.topHits();
                    if (topHitsAggregate == null){
                        return homePageVo;
                    }
                    HitsMetadata<JsonData> hits = topHitsAggregate.hits();
                    if (hits == null) {
                        return homePageVo;
                    }
                    List<Hit<JsonData>> hitList = hits.hits();
                    if (CollectionUtils.isEmpty(hitList)) {
                        return homePageVo;
                    }
                    homePageVo.setList(hitList.stream().map(hit -> hit.source().to(AlbumInfoIndex.class)).collect(Collectors.toList()));

                    return homePageVo;
                }).collect(Collectors.toList());
            }

            return null;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public AlbumSearchResponseVo search(AlbumIndexQuery indexQuery) {

        try {
            // 执行搜索
            SearchResponse<AlbumInfoIndex> response = this.elasticsearchClient.search(this.buildDsl(indexQuery), AlbumInfoIndex.class);
            // 解析结果集
            AlbumSearchResponseVo responseVo = this.parseResult(response);
            // 分页参数
            responseVo.setPageNo(indexQuery.getPageNo());
            responseVo.setPageSize(indexQuery.getPageSize());
            // 总页数
            long pages = responseVo.getTotal() / responseVo.getPageSize();
            responseVo.setTotalPages(responseVo.getTotal() % responseVo.getPageSize() == 0 ? pages : pages + 1);
            return responseVo;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public List<AlbumInfoIndexVo> findRankingList(Long category1Id, String field) {

        try {
            SearchResponse<AlbumInfoIndex> response = this.elasticsearchClient.search(s -> s
                            .index("albuminfo")
                            .query(q -> q.term(t -> t.field("category1Id").value(category1Id)))
                            .size(20)
                            .sort(st -> st.field(f -> f.field(field).order(SortOrder.Desc)))
                    , AlbumInfoIndex.class);

            // 解析结果集
            List<Hit<AlbumInfoIndex>> hitList = response.hits().hits();
            return hitList.stream().map(hit -> {
                AlbumInfoIndexVo albumInfoIndexVo = new AlbumInfoIndexVo();
                // 获取每一条数据
                AlbumInfoIndex albumInfoIndex = hit.source();
                BeanUtils.copyProperties(albumInfoIndex, albumInfoIndexVo);

                return albumInfoIndexVo;
            }).collect(Collectors.toList());

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public List<String> completeSuggest(String prefix) {
        List<String> suggests = new ArrayList<>();
        // 如果用户录入的前缀为空，则直接返回空集合
        if (StringUtils.isBlank(prefix)){
            return suggests;
        }

        try {
            // 拼装DSL语句并执行搜索，获取搜索结果集
            SearchResponse<SuggestIndex> response = this.elasticsearchClient.search(s -> s
                            .suggest(sg -> sg
                                    .suggesters("keywordSuggest", ks -> ks
                                            .prefix(prefix)
                                            .completion(c -> c
                                                    .field("keyword")
                                                    .size(10)
                                                    .skipDuplicates(true)))
                                    .suggesters("keywordPinyinSuggest", ks -> ks
                                            .prefix(prefix)
                                            .completion(c -> c
                                                    .field("keywordPinyin")
                                                    .size(10)
                                                    .skipDuplicates(true)))
                                    .suggesters("keywordSequenceSuggest", ks -> ks
                                            .prefix(prefix)
                                            .completion(c -> c
                                                    .field("keywordSequence")
                                                    .size(10)
                                                    .skipDuplicates(true)))
                            ),
                    SuggestIndex.class);

            // 解析结果集
            Map<String, List<Suggestion<SuggestIndex>>> suggestMap = response.suggest();
            // 中文
            Suggestion<SuggestIndex> keywordSuggest = suggestMap.get("keywordSuggest").get(0);
            // 拼音
            Suggestion<SuggestIndex> keywordPinyinSuggest = suggestMap.get("keywordPinyinSuggest").get(0);
            // 首字母
            Suggestion<SuggestIndex> keywordSequenceSuggest = suggestMap.get("keywordSequenceSuggest").get(0);

            // 从三个suggestion中获取提示词集合 放入一个大的set去重
            Set<String> suggestSet = new HashSet<>();
            suggestSet.addAll(getTitlesFromSuggestion(keywordSuggest));
            suggestSet.addAll(getTitlesFromSuggestion(keywordPinyinSuggest));
            suggestSet.addAll(getTitlesFromSuggestion(keywordSequenceSuggest));
            suggests.addAll(suggestSet);

            // 判断提示词是否太少，太少则根据关键字进行搜索，把相关记录作为提示词显示
            if (suggests.size() >= 10){
                return suggests;
            }
            SearchResponse<AlbumInfoIndex> searchResponse = this.elasticsearchClient.search(s -> s.index("albuminfo")
                            .query(q -> q.fuzzy(f -> f.field("albumTitle").value(prefix).fuzziness("2"))),
                    AlbumInfoIndex.class);
            List<Hit<AlbumInfoIndex>> hits = searchResponse.hits().hits();
            hits.forEach(hit -> {
                if (suggests.size() >= 10){
                    return;
                }
                suggests.add(hit.source().getAlbumTitle());
            });

            return suggests;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void updateRanking() {
        // 获取redis中的上一次的截止时间作为这次统计的开始时间
        String startTime = (String) this.redisTemplate.opsForValue().get(RedisConstant.ALBUM_STAT_ENDTIME);
        if (StringUtils.isBlank(startTime)){
            // 如果为空，则使用默认的起始时间
            startTime = "2024-03-08 07:23:53";
        }
        // 获取截止时间，缓存到redis作为下一次的起始时间
        String endTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        this.redisTemplate.opsForValue().set(RedisConstant.ALBUM_STAT_ENDTIME, endTime);

        // 查询统计信息发生变化的专辑id集合
        Result<List<Long>> albumIdsResult = this.albumInfoFeignClient.getChangedAlbumIds(startTime, endTime);
        Assert.notNull(albumIdsResult, "同步统计信息到es时，获取变化的专辑id失败！");
        List<Long> albumIds = albumIdsResult.getData();
        if (CollectionUtils.isEmpty(albumIds)){
            return;
        }
        // 如果不为空，则要同步到es
        List<List<Long>> partition = Lists.partition(albumIds, 5000);
        partition.forEach(subAlbumIds -> {
            executorService.execute(() -> {
                // 获取该批次的专辑统计信息
                Result<List<AlbumStatVo>> albumStatsResult = this.albumInfoFeignClient.getAlbumStatsByAlbumIds(subAlbumIds);
                Assert.notNull(albumStatsResult, "同步统计信息到es时，获取统计信息失败！");
                List<AlbumStatVo> albumStatVos = albumStatsResult.getData();
                if (CollectionUtils.isEmpty(albumStatVos)){
                    return ;
                }
                albumStatVos.forEach(albumStatVo -> {
                    AlbumInfoIndex albumInfoIndex = new AlbumInfoIndex();
                    BeanUtils.copyProperties(albumStatVo, albumInfoIndex);
                    albumInfoIndex.setId(albumStatVo.getAlbumId());
                    // 设置热度
                    albumInfoIndex.setHotScore(0.1 * albumStatVo.getPlayStatNum() + 0.2 * albumStatVo.getSubscribeStatNum()
                            + 0.3 * albumStatVo.getCommentStatNum() + 0.4 * albumStatVo.getBuyStatNum());
                    this.elasticsearchTemplate.update(albumInfoIndex);
                });
            });
        });

    }

    private List<String> getTitlesFromSuggestion(Suggestion<SuggestIndex> keywordSuggest) {
        // 获取提示词中的options
        List<CompletionSuggestOption<SuggestIndex>> options = keywordSuggest.completion().options();
        // 中文提示词集合
        return options.stream().map(option -> option.source().getTitle()).collect(Collectors.toList());
    }

    /**
     * 构建DSL语句
     * @param indexQuery
     * @return
     */
    private SearchRequest buildDsl(AlbumIndexQuery indexQuery){

        // api语法
        SearchRequest.Builder requestBuilder = new SearchRequest.Builder();
        requestBuilder.index("albuminfo"); // 指定索引库

        // 1.构建查询过滤：布尔查询构建器
        BoolQuery.Builder boolQueryBuilder = new BoolQuery.Builder();
        // 1.1. 构建多字段匹配查询
        String keyword = indexQuery.getKeyword();
        if (StringUtils.isNotBlank(keyword)){
            boolQueryBuilder.must(m -> m.multiMatch(mm -> mm
                    .query(keyword)
                    .fields("albumTitle",
                            "albumIntro",
                            "announcerName")
                    .operator(Operator.And)));
        }

        // 1.2. 构建过滤
        // 1.2.1. 三级分类的过滤
        Long category1Id = indexQuery.getCategory1Id();
        if (category1Id != null) {
            boolQueryBuilder.filter(f -> f.term(t -> t.field("category1Id").value(category1Id)));
        }
        Long category2Id = indexQuery.getCategory2Id();
        if (category2Id != null) {
            boolQueryBuilder.filter(f -> f.term(t -> t.field("category2Id").value(category2Id)));
        }
        Long category3Id = indexQuery.getCategory3Id();
        if (category3Id != null) {
            boolQueryBuilder.filter(f -> f.term(t -> t.field("category3Id").value(category3Id)));
        }
        // 1.2.2. 标签的嵌套过滤
        List<String> attributeList = indexQuery.getAttributeList();
        // ["2:4", "3:6", "4:8"]
        if (!CollectionUtils.isEmpty(attributeList)){
            // 遍历每一个元素，每一元素就是一个嵌套过滤
            attributeList.forEach(attribute -> { // 2:4
                // 对字符串进行拆分
                String[] attrs = StringUtils.split(attribute, ":");
                if (attrs != null && attrs.length == 2 && StringUtils.isNumeric(attrs[0]) && StringUtils.isNumeric(attrs[1])){
                    boolQueryBuilder.filter(f -> f.nested(n -> n
                            .path("attributeValueIndexList")
                            .query(q -> q.bool(b -> b
                                    .must(m -> m.term(t -> t.field("attributeValueIndexList.attributeId").value(attrs[0])))
                                    .must(m -> m.term(t -> t.field("attributeValueIndexList.valueId").value(attrs[1])))))));
                }
            });
        }

        // 再由查询构建器 构建出布尔查询放入requestBuilder对象
        requestBuilder.query(boolQueryBuilder.build()._toQuery());

        // 2.构建排序 综合排序[1:desc] 播放量[2:desc] 发布时间[3:desc]
        String order = indexQuery.getOrder();
        if (StringUtils.isNotBlank(order)){
            String[] orders = StringUtils.split(order, ":");
            List<String> sorts = Arrays.asList("asc", "desc");
            if (orders != null && orders.length == 2 && sorts.contains(orders[1])){
                String fieldName = null;
                switch (orders[0]){
                    case "1": fieldName = "hotScore"; break;
                    case "2": fieldName = "playStatNum"; break;
                    case "3": fieldName = "createTime"; break;
                }
                String finalFieldName = fieldName;
                requestBuilder.sort(s -> s.field(f -> f.field(finalFieldName).order(StringUtils.equals(orders[1], "desc") ? SortOrder.Desc : SortOrder.Asc)));
            }
        }

        // 3.构建分页
        Integer pageNo = indexQuery.getPageNo();
        Integer pageSize = indexQuery.getPageSize();
        requestBuilder.from((pageNo - 1) * pageSize);
        requestBuilder.size(pageSize);

        // 4.高亮
        requestBuilder.highlight(h -> h
                .fields("albumTitle", f -> f
                        .preTags("<font style='color:red'>")
                        .postTags("</font>"))
                .fields("albumIntro", f -> f
                        .preTags("<font style='color:red'>")
                        .postTags("</font>")));

        // 5.构建结果集过滤
        requestBuilder.source(s -> s.filter(f -> f.excludes("createTime")));

        SearchRequest request = requestBuilder.build();
        System.out.println(request);
        return request;
    }

    private AlbumSearchResponseVo parseResult(SearchResponse<AlbumInfoIndex> response){
        AlbumSearchResponseVo responseVo = new AlbumSearchResponseVo();

        // 搜索的命中结果集
        HitsMetadata<AlbumInfoIndex> hits = response.hits();

        // 设置总记录数
        responseVo.setTotal(hits.total().value());

        // 搜索结果集中当前页的数据
        List<Hit<AlbumInfoIndex>> hitList = hits.hits();
        List<AlbumInfoIndexVo> list = hitList.stream().map(hit -> {
            AlbumInfoIndex albumInfoIndex = hit.source();
            AlbumInfoIndexVo albumInfoIndexVo = new AlbumInfoIndexVo();
            BeanUtils.copyProperties(albumInfoIndex, albumInfoIndexVo);

            // 高亮字段的解析
            Map<String, List<String>> highlight = hit.highlight();
            if (CollectionUtils.isEmpty(highlight)){
                return albumInfoIndexVo;
            }
            // 如果高亮结果集不为空，则进行高亮解析
            List<String> albumTitles = highlight.get("albumTitle");
            if (!CollectionUtils.isEmpty(albumTitles)){
                albumInfoIndexVo.setAlbumTitle(albumTitles.get(0));
            }
            List<String> albumIntros = highlight.get("albumIntro");
            if (!CollectionUtils.isEmpty(albumIntros)){
                albumInfoIndexVo.setAlbumIntro(albumIntros.get(0));
            }

            return albumInfoIndexVo;
        }).collect(Collectors.toList());
        responseVo.setList(list);

        return responseVo;
    }
}
